/**
 *  代理服务器的线程.传入一个ServerSocket,该线程将监听并处理.
 *  监听后立即新建另一个相同的线程,重新监听.
 */
package setycyas.proxy;

import java.io.*;
import java.net.*;
import java.util.*;

/**
 * @author setycyas 2018-08-10
 * 
 * 基本完成的代理服务器,运行静态main即可.
 * 原理简单,用socket实现,几乎是直接交换数据.注意重点:
 * 
 * 1.https请求时,首个请求是connect,不能直接发数据去服务器,需要先回应:
 * "HTTP/1.1 200 Connection Established\r\n\r\n",不能漏换行!我就这里踩了坑.
 * 
 * 2.单线程不行,需要2个,一个把客户端的输入交给服务器,另一个把服务器输入交个客户端.
 * 
 * 3.监听成功后,立即新建线程继续监听.因为只有一个线程能监听,所以不能同时开多个监听的.
 * 只有在自己的监听成功后,才能开新的.要是不开,代理服务器就无法响应新请求了.
 * 所以每次客户端的都需要新线程.
 * 
 * 要注意的重点只有这3个.但细节还是很耐写的.
 * 
 * 另外,阻塞中关闭线程的方法,看:https://blog.csdn.net/al_assad/article/details/52992546
 * 这里关键是流的读写阻塞很多,把流关闭了,就一切清净.这是让所有线程通过异常终止的最简单办法!
 */
public class ProxyThread extends Thread{

	/* 静态常量 */
	
	// 测试记录文件夹
	private static final String TESTFOLDER
		= "D:/MyDocument/MyCode/EclipseWorkspace/JavaTest/TestFiles/proxyRequests/";
	// 测试用端口默认值
	private static final int TESTPORT = 2234;
	// 读取客户端的buffer的默认大小
	private static final int CLIENTBUFFER_SIZE = 4*1024;
	// 读取客户端的buffer的默认大小
	private static final int SERVERBUFFER_SIZE = 4*1024;
	// 服务器默认端口
	private static final int SERVERDEFAULT_PORT = 80;

	/* 静态变量 */
	// 所有子线程的集合,最终关闭所有时使用
	private static HashSet<Thread> proxyThreadSet = new HashSet<Thread>();
	
	/* 实例变量 */

	// 首次访问方法,暂时只考虑是否connect
	String firstMethod = "";
	// 服务器port
	int serverPort = -1;
	// 服务器主机名
	String serverHost = "";
	// 线程id,输出时识别用
	public final int id;
	// 线程名称,跟id相关,在构造函数初始化,文字输出用
	public final String name;
	// 终止符
	boolean stopFlag = false;
	// 代理服务器的serverSocket
	private ServerSocket ss = null;
	// 对客户端的socket
	private Socket client = null;
	// 对服务器的socket
	private Socket server = null;
	// 读入客户端消息的buffer
	byte[] clientReadBuffer = new byte[CLIENTBUFFER_SIZE];
	// 从客户端上次读入的信息长度
	int clientBytesRead = -1;
	// 读入客户端消息的buffer
	byte[] serverReadBuffer = new byte[SERVERBUFFER_SIZE];
	// 从客户端上次读入的信息长度
	int serverBytesRead = -1;
	// 与客户端交互的stream
	InputStream clientInputStream = null;
	OutputStream clientOutputStream = null;
	// 与服务器交互的stream
	InputStream serverInputStream = null;
	OutputStream serverOutputStream = null;
	// 记录client传送消息的文件名与输出stream
	private String clientRecordFile = null;
	private FileOutputStream clientRecordOutputStream = null;
	// 记录从client与server读取的总字节数
	int clientTotalRead = 0;
	int serverTotalRead = 0;
	
	/* 实例公有变量 */
	
	// 文字编码,随意修改,解码失败就抛异常
	public String charSet = "UTF-8";
			
	/* 构造函数,初始化各种变量 */
	public ProxyThread(ServerSocket ss, int id, String recordFolder) {
		super();
		this.ss = ss;
		if(id > 1 ) {
			this.id = id;
		}else {
			this.id = 1;
		}
		this.name = "ProxyThread-"+this.id;
		this.clientRecordFile = recordFolder+name+"clientInputRecord";
		File fout = new File(clientRecordFile);
		try {
			if(!fout.exists())
				fout.createNewFile();
			this.clientRecordOutputStream = new FileOutputStream(fout);
		}catch(Exception e) {
			this.stopFlag = true;
			e.printStackTrace();
		}
	}
	public ProxyThread(ServerSocket ss, int id) {
		this(ss, id, TESTFOLDER);
	}

	/* 把一个字节数组按长度变成字符串,注意编码定义在类的公有变量中 */
	private String stringFromBytes(byte[] bs, int bsLen) throws UnsupportedEncodingException {
		if(bs.length > bsLen) {
			byte[] newBs = new byte[bsLen];
			for(int i = 0;i < bsLen;i++)
				newBs[i] = bs[i];
			return new String(newBs, charSet);
		}else {
			return new String(bs, charSet);
		}
	}
	
	/* 处理主机与端口组合的字符串,获取主机与端口 */
	private void handleHostAndPortString(String hostAndPortString) {
		String str = hostAndPortString.trim().toLowerCase();
		String[] strSplit = str.split(":");
		this.serverHost = strSplit[0];
		if (strSplit.length > 1) {
			this.serverPort = Integer.parseInt(strSplit[1]);
		}else {
			this.serverPort = SERVERDEFAULT_PORT;
		}	
		return;
	}
	
	/* 处理首次访问的字节数组,获取访问方法,主机,端口 */
	private void handleFirstBytes(byte[] bs,int bsLen) {
		try {
			 // 主机与端口组合的字符串,预定义,将来用于分析
			String hostAndPort = null;
			// 变成字符串,按换行分割.
			String src = stringFromBytes(bs, bsLen).toLowerCase();
			String[] lines = src.split("\n");
			String firstLine = lines[0];
			String[] firstLineSplit = firstLine.split(" ");
			// 获取首次访问方法
			this.firstMethod = firstLineSplit[0];
			// 获取主机与端口字符串.考虑Connect方法,再考虑非Connect方法
			if (this.firstMethod.equals("connect")) {
				hostAndPort = firstLineSplit[1];
			}else {
				for(int i = 0;i < lines.length;i++) {
					if(lines[i].startsWith("host")) {
						hostAndPort=lines[i].split(":",2)[1].trim();
						break;
					}
				}
			}
			// 处理hostAndPort字符串
			this.handleHostAndPortString(hostAndPort);
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
				System.err.println(name+"客户端首次Input解码失败!");
				this.stopFlag = true;
			}
			return;
		}	
	
	/* 处理客户端的首次输入,建立与客户端的连接,然后建立与服务器的连接.
	 * 如果是CONNECT方法,建立对服务器的连接,并向客户端返回:
	 * "HTTP/1.1 200 Connection Established\r\n\r\n",不能漏换行!
	 * 如果不是CONNECT(功能不全,直接当作GET),建立连接,客户端的首次输入也要发送给服务器.
	*/
	private void handleFirstClientInput() throws IOException {
		// 建立与客户端的连接
		this.clientInputStream = client.getInputStream();
		this.clientOutputStream  = client.getOutputStream();
		System.out.println(name+"首次接收client数据,成功获取client的InputStream与OutputStream");
		// 读取一次客户端数据
		clientBytesRead = clientInputStream.read(clientReadBuffer);
		this.clientTotalRead += clientBytesRead;
		this.clientRecordOutputStream.write(clientReadBuffer, 0, clientBytesRead);
		System.out.println(name+"首次接收的client数据已写入记录!");
		// 处理第一次获取的数据,获取访问方法,服务器主机,端口.这些都在类的变量中保存.
		this.handleFirstBytes(clientReadBuffer, clientBytesRead);
		// 与服务器建立连接
		this.server = new Socket(this.serverHost,this.serverPort);	
		this.serverOutputStream = server.getOutputStream();
		this.serverInputStream = server.getInputStream();
		System.out.println("连接到Server - "+serverHost+":"+serverPort);
		// 如果是connect方法首次访问,向客户端回应OK.
		// 否则,直接向服务器发送首次请求的数据
		if(this.firstMethod.equals("connect")) {
			String reply = "HTTP/1.1 200 Connection Established\r\n\r\n";
			this.clientOutputStream.write(reply.getBytes());
			System.out.println("客户端的首次访问方法是Connect,已作出响应.");
		}else {
			this.serverOutputStream.write(clientReadBuffer, 0, clientBytesRead);
		}
		// 完成
		return;
	}
	
	@SuppressWarnings("deprecation")
	@Override
	public void run() {
	  try {
		System.out.println(name+" start to run");
		proxyThreadSet.add(this);
		if (stopFlag) {
			System.out.println(name+"构造函数出了问题,终止");
			proxyThreadSet.remove(this);
			return;
		}
		// 预定义一个接收客户端输入的线程,将来中断用.
		ServerInputStreamTransfer serverInputStreamTransfer = null;
		try {
			this.client = ss.accept();
			// 监听成功后,开一个新线程监听下一个客户端访问
			(new ProxyThread(ss,id+1)).start();
			// 处理第一次得到的客户端数据
			handleFirstClientInput();
			System.out.println(name+"处理客户端首次输入完成!");
			// 启动读取服务器数据的线程,而读取客户端数据将在本线程执行.
			// 理由简单:客户端输入决定代理服务器的行为,更重要,在主线程处理.
			System.out.println(name+"开启读取server输入的新线程!");
			serverInputStreamTransfer = new ServerInputStreamTransfer(this);
			proxyThreadSet.add(serverInputStreamTransfer);
			serverInputStreamTransfer.start();
			// 当前线程读取client的输入
			while((clientBytesRead = clientInputStream.read(clientReadBuffer)) != -1) {
				this.serverOutputStream.write(clientReadBuffer, 0, clientBytesRead);
				this.clientTotalRead += clientBytesRead;
				this.clientRecordOutputStream.write(clientReadBuffer, 0, clientBytesRead);
				if (this.stopFlag) break;
			}
		} catch (Exception e) {
			e.printStackTrace();
			System.err.println(name+"出现错误!");
		}finally {
			System.out.println(name+"准备结束!");
			// 分析结束原因,关闭子线程
			if (!this.stopFlag) {
				System.out.println(name+"的结束是由client连接断开引起的");
				if (serverInputStreamTransfer.isAlive()) {
					serverInputStreamTransfer.stop();
				}
			}else {
				if (serverInputStreamTransfer.isAlive()) {
					System.out.println(name+"的结束是由外部命令引起");
					serverInputStreamTransfer.stop();
				}else {
					System.out.println(name+"的结束是由server连接断开");
				}
			}
			// 结束:关闭所有流,终止server读取线程
			try {
				this.serverInputStream.close();
				this.clientInputStream.close();
				this.serverOutputStream.close();
				this.clientOutputStream.close();
				this.clientRecordOutputStream.close();
			}catch(Exception e) {
				e.printStackTrace();
				System.err.println("关闭流的过程出现错误!");
			}
		}
		proxyThreadSet.remove(this);
		proxyThreadSet.remove(serverInputStreamTransfer);
		System.out.println(name+"结束!!!");
		System.out.println(name+"统计:从客户端读取字节数: "+this.clientTotalRead
				+" 从服务器读取字节数: "+this.serverTotalRead);	
	  }catch(Exception e) {
		e.printStackTrace();  
	  }
	}	
	
	/**
	 * 入口main函数
	 * @throws IOException 
	 */
	@SuppressWarnings("resource")
	public static void main(String[] args) {
		// TODO 基本完成,测试中
		int port = TESTPORT;	
		ProxyThread pt = null;
		ServerSocket ss = null;
		int threadId = 888;
		try {
			ss = new ServerSocket(port);
			pt = new ProxyThread(ss,threadId);
			pt.start();
		} catch (IOException e) {
			e.printStackTrace();
		}
		// 监听键盘对控制台的输入,一旦有回车输入,终止代理服务器!这样所有线程的读写流都关闭了,
		// 所有线程都将异常终止.是关闭主线程的最好方法.
		(new Scanner(System.in)).nextLine();
		try {
			ss.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("主线程结束!!!");
	}
	

}

/* 把server的input数据往client的output传送的子线程,需要主线程数据支持 */
class ServerInputStreamTransfer extends Thread{
	
	private ProxyThread proxy = null;
	
	ServerInputStreamTransfer(ProxyThread proxy){
		super();
		this.proxy = proxy;
		
	}

	@Override
	public void run() {
		InputStream ins = proxy.serverInputStream;
		OutputStream outs = proxy.clientOutputStream;
		byte[] buffer = proxy.serverReadBuffer;
		if ((ins == null) || (outs == null)) {
			System.err.println(proxy.name+"创建server读取线程时,输入输出流为空!");
			return;
		}
		// 不断读取server的输入流,交给客户端
		try {
			while((proxy.serverBytesRead = ins.read(buffer)) != -1) {
				outs.write(buffer, 0, proxy.serverBytesRead);
				proxy.serverTotalRead += proxy.serverBytesRead;
				if (proxy.stopFlag) break;
			}
		}catch(IOException e) {
			e.printStackTrace();
		}
		proxy.stopFlag = true;
	}
	
}
